PyCon JP 2018: Webアプリケーションの仕組み
.
https://gyazo.com/e317cddc08ed302633366fde592d0364
おまえ誰よ
活動:
Python関連書籍の翻訳と執筆
https://gyazo.com/0c7c457d5f57c1fb162d3f9bc7911d78
https://gyazo.com/5fe0b66a6b9804ea8c4a49091d333879
アジェンダ
ブラウザからのリクエストに応答する
cookieとセッション
データ保存
まとめ
最近のWebアプリ開発
上から下まで幅広い範囲の知識が必要
とは言うけれど.. やること多すぎ!
https://gyazo.com/736450ff7fcf51f087b1b51d05d6444c
https://gyazo.com/286ddcbb0079cb5710f2ff6e28deeb1e
Webフレームワークの機能
View
https://gyazo.com/736450ff7fcf51f087b1b51d05d6444c
質問1
質問2
1. だいたい把握してる -> 1人
2. すこし把握してる -> 5人
3. 全然わからない、雰囲気で使っている -> 50人以上
DjangoやFlaskの機能範囲
Django
https://gyazo.com/4994265e9ccbbe9e659f75ee5c534ea2
Flask
https://gyazo.com/47954898a618341b0d9dfbe25c6adae7
機能多い
ドキュメント量で機能を計測
この膨大なドキュメント読んで把握とか難しい
ドキュメントの量 == 難易度 ?
把握できないから比較できない?
背景が分からないから便利な感じがしない?
ゼロから自作して追体験しよう
なにも無かった時代はどうやって作っていた?
フレームワークのない2000年頃
Web黎明期のシンプルな世界
2000年頃、Web黎明期には色々なかった
Webサイトの要件(現在)
動的ページ:
同時アクセス:
HTML, CSS, 画像と多数のリクエストをさばく必要がある
性能:
セキュリティーチェックや、ページ組み立てなど、やることが多い
可用性:
サイトが落ちてるとTwitterで話題にされる
Webサイトの要件(黎明期)
動的ページ:
同時アクセス:
同時1接続でもまあなんとかなる
性能:
遅くなるほど複雑なことをしない
可用性:
たまにサイト落ちてても立ち上げ直せばOK
やってみよう
データ保存
ブラウザの動作を観察
何が起きている?(SNSで最近話題のやつ)
内部で色々な通信が発生している
ブラウザのデバッガーで確認
Webサーバーの動作観察
telnetを使う
サーバーにアクセスして
code:telnet(http)
$ telnet example.com 80
Trying 93.184.216.34...
Connected to example.com.
Escape character is '^]'.
GET / HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Sun, 09 Sep 2018 05:56:41 GMT
Etag: "1541025663+gzip+ident"
Expires: Sun, 16 Sep 2018 05:56:41 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: ECS (sjc/4E8D)
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1270
<!doctype html>
<html>
<head>
<title>Example Domain</title>
...
Webサーバーの動作観察
Pythonでサイトアクセス (1/2)
例として http://example.com にアクセス
code:open-example-com (python)
$ python3
Python 3.6.6 (v3.6.6:4cf1f54eb7, Jun 26 2018, 19:50:54)
Type "help", "copyright", "credits" or "license" for more information.
>> from urllib.request import urlopen
>> uo.status
200
>> uo.headers.items()
('Cache-Control', 'max-age=604800'), ('Content-Type', 'text/html; charset=UTF-8'), ('Date', 'Sun, 09 Sep 2018 05:59:50 GMT'), ('Etag', '"1541025663+ident"'), ('Expires', 'Sun, 16 Sep 2018 05:59:50 GMT'), ('Last-Modified', 'Fri, 09 Aug 2013 23:54:35 GMT'), ('Server', 'ECS (oxr/8313)'), ('Vary', 'Accept-Encoding'), ('X-Cache', 'HIT'), ('Content-Length', '1270'), ('Connection', 'close') HTTPステータスが200 (OK)、Content-Typeがtext/html; charset=UTF-8 なのが分かる Webサーバーの動作観察
Pythonでサイトアクセス (2/2)
code:open-example-com (python)
>> print(uo.read().decode('utf-8'))
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
...
HTMLが返ってきていることが分かる
Webサーバーを作る
1. socketをTCP 8000番ポートで開く 4. ブラウザからアクセスする
Webサーバーを作る
socketを開く
code:webapp0.py
import socket
def view(raw_request):
print(raw_request)
return 'HTTP/1.1 501\r\n\r\nSorry\n'
def main():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('127.0.0.1', 8000))
s.listen()
while True:
conn, addr = s.accept()
with conn:
raw_request = b''
while True:
chunk = conn.recv(4096)
raw_request += chunk
if len(chunk) < 4096:
break
raw_response = view(raw_request.decode('utf-8'))
conn.sendall(raw_response.encode('utf-8'))
if __name__ == '__main__':
main()
参考:
Webサーバーを作る
実行と確認
code:shell
$ python3 webapp0.py
ブラウザで http://127.0.0.1:8000/ にアクセス
ブラウザに Sorry と表示される
テキトウなHTTPレスポンス
ブラウザで http://127.0.0.1/ にアクセスするたびに、異なるエラー、異なる文字が表示される
code:webapp0b.py
import random
def view(raw_request):
print(raw_request)
resp_list = [
'HTTP/1.1 404 Not Found\r\n\r\nNo Page\n',
'HTTP/1.1 402 Payment Required\r\n\r\nOkane Choudai\n',
'HTTP/1.1 501 Not Implemented\r\n\r\nMada Dayo\n',
]
resp = random.choice(resp_list)
return resp
# 省略
HTMLを返す
code:webapp1.py
def view(raw_request):
print(raw_request)
resp = '''HTTP/1.1 200 OK
<html><body>
<h1>Hello World!</h1>
</body></html>
'''
return resp
HTTPリクエストのパスを見て / 以外は404を返す
code:webapp2.py
def view(raw_request):
header, body = raw_request.split('\r\n\r\n', 1) # 最初のCRLFで分割
print(header)
print(body)
headers = header.splitlines()
# リクエストラインを分割
method, path, version = headers0.split(' ', 2) if path == '/':
resp = dedent('''\
HTTP/1.1 200 OK
<html><body>
<h1>Hello World!</h1>
</body></html>
''')
else:
resp = dedent('''\
HTTP/1.1 404 NOT FOUND
NO PAGE
''')
return resp
リクエスト/レスポンス処理をちょっと整理
requestの解析とresponseの組立てを関数化
code:webapp3.py
import socket
def make_request(raw_request):
if isinstance(raw_request, bytes):
raw_request = raw_request.decode('utf-8')
print(raw_request)
header, body = raw_request.split('\r\n\r\n', 1)
headers = header.splitlines()
method, path, proto = headers0.split(' ', 2) request = {
'body': body,
'REQUEST_METHOD': method,
'PATH_INFO': path,
'SERVER_PROTOCOL': proto,
}
return request
def make_response(status, headers, body):
status_line = ('HTTP/1.1 ' + status).encode('utf-8')
hl = []
for k, v in headers:
h = '%s: %s' % (k, v)
hl.append(h)
header = ('\r\n'.join(hl)).encode('utf-8')
if isinstance(body, str):
body = body.encode('utf-8')
raw_response = status_line + b'\r\n' + header + b'\r\n\r\n' + body
print(raw_response)
return raw_response
def view(request):
body = '''
<html><body>
<h1>Hello World!</h1>
</body></html>
'''
else:
return resp # (status str, headers tuple, content)
def app(raw_request):
request = make_request(raw_request)
status, headers, body = view(request)
if isinstance(body, str):
body = body.encode('utf-8')
raw_response = make_response(status, headers, body)
return raw_response
def main():
...
# raw_response = view(raw_request.decode('utf-8'))
raw_response = app(raw_request)
# conn.sendall(raw_response.encode('utf-8'))
conn.sendall(raw_response)
if __name__ == '__main__':
main()
HTMLとCSSと画像を表示する
HTMLにcssファイルと画像ファイルへのリンクを追加
URLとのマッピング
ファイルアクセス
code:webapp4.py
def view(request):
body = '''
<html>
<head>
<link href="/static/style.css" rel="stylesheet">
</head>
<body>
<h1>Hello World!</h1>
<img src="/static/image.jpg">
</body></html>
'''
headers = [
('Content-Type', 'text/css'),
]
resp = ('200 OK', headers, open('static/style.css', 'rb').read())
headers = [
('Content-Type', 'image/jpg'),
]
resp = ('200 OK', headers, open('static/image.jpg', 'rb').read())
else:
return resp
URLのパスでview関数を分ける
code:webapp5.py
import os
from mimetypes import guess_type
...
def index_view(request):
body = '''
<html>
<head>
<link href="/static/style.css" rel="stylesheet">
</head>
<body>
<h1>Hello World!</h1>
<img src="/static/image.jpg">
</body></html>
'''
return ('200 OK', [], body)
def file_view(request):
path = path.lstrip('/') # remove first /
if not os.path.isfile(path):
return notfound_view(request)
ct, _ = guess_type(path)
if ct is None:
ct = 'application/octet-stream'
headers = [
('Content-Type', ct),
]
return ('200 OK', headers, open(path, 'rb').read())
def notfound_view(request):
return ('404 NOT FOUND', [], 'NO PAGE')
patterns = {
'/static/': file_view,
'/': index_view,
}
def dispatch(request):
for path, view in patterns.items():
if path_info.startswith(path):
return view
return notfound_view
def app(raw_request):
request = make_request(raw_request)
view = dispatch(request) # 追加
status, headers, body = view(request)
if isinstance(body, str):
body = body.encode('utf-8')
raw_response = make_response(status, headers, body)
return raw_response
現在の要件を満たす
同時アクセス: HTMLだけでなくCSSや画像も表示するので多重アクセスできないとページ表示が重い
可用性: サイトが落ちてるとTwitterで話題にされる
信頼性: セキュリティーの向上
性能: 遅いと文句言われる
ライブラリに任せよう
プロセスが死んでも生き返る
こういった機能を自分で実装せずに済む
Gunicornから自作Webアプリを起動する
https://gyazo.com/0be06ef21674581439344d215a2efa70
自作WebアプリもWSGI準拠にすればGunicornから起動できる code:webapp5wsgi.py
...
def wsgiapp(environ, start_response):
request = environ
view = dispatch(request)
status, headers, body = view(request)
if isinstance(body, str):
body = body.encode('utf-8')
start_response(status, headers)
...
起動: gunicorn -w 2 webapp5wsgi:application
より高速で堅牢なサービス提供
高速な静的ファイル配信
省メモリ
etc..
https://gyazo.com/286ddcbb0079cb5710f2ff6e28deeb1e
ここからcookieとsessionの話
cookie
code:HTTPレスポンスヘッダー (http)
Set-Cookie: SID=31d4d96e407aad42
Set-Cookie: name=清水川
code:HTTPリクエストヘッダー(http)
Cookie: SID=31d4d96e407aad42
Cookie: name=清水川
session
sessionは、特定ユーザーの情報を決められた期間だけ保存しておく入れ物 セッションデータをcookieに保存
code:HTTPレスポンスヘッダーでsessionを持たせる(HTTP)
Cookie: session=V2Vi44Ki44OX44Oq44Kx44O844K344On44Oz44Gu5LuV57WE44G/==\n
データはユーザーに閲覧されてしまうし、書き換えられる
セッションデータをWebサーバーに保存
メモリ
ファイル:
/tmp/session-31d4d96e407aad42 等
気づくと /tmp がDISK FULLになったり
セッションデータをKVS等に保存
名前は何でも良いけど、Djangoのデフォルトでは sessionid が使われる
HTTPヘッダー
code:HTTPレスポンスヘッダーでsessionidを持たせる(http)
Set-Cookie: sessionid=31d4d96e407aad42439850e9df4354
code:HTTPリクエストヘッダーでsessionidを伝える(http)
Cookie: sessionid=31d4d96e407aad42439850e9df4354
session tokenを複製すると別ブラウザでもログイン状態になれる
session tokenを複製してアクセス
Pythonでもcookieに複製したsessionidを入れてサーバーアクセスすれば、ログイン状態になれる
code:clone-session.py
import requests
c = {'sessionid': 'pfcrhqghmflwb......'}
print(res.text)
データ保存の話
シンプルで分かりやすい
まとめ
今のWebアプリケーション開発に使われるフレームワークやスタックがなぜ必要とされているか、どのような利点があるのか
https://gyazo.com/736450ff7fcf51f087b1b51d05d6444c
これからも開発を高速に進めるために
応用するのに必要
いつ学ぶ?
参考文献
HTTPプロトコルの基礎を学んで、いまのWeb技術がどのように作られていったのか、現在どのように利用されているのかを知り、時代の変化に左右されない、Webの基礎技術を学ぶ。 動画
https://www.youtube.com/watch?v=L7j2zgtpV9c
記事